import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;

public class BufferPool {

	private final int BLOCK_SIZE = 4096;
	private final int RECORD_SIZE = 4;
	private BufferNode head, tail;
	private int hits, misses, reads, writes;
	private int max_count, cur_count;
	private RandomAccessFile file;

	public BufferPool(int num_blocks, String file_name)
			throws FileNotFoundException {
		max_count = num_blocks;
		file = new RandomAccessFile(file_name, "rws");
		initCacheCount();
		cur_count = 0;
		head = tail = null;
	}

	public int read(long index) throws IOException {
		long addr = index * RECORD_SIZE;
		BufferNode temp = checkHit(addr);
		if (temp == null) {
			if (cur_count == max_count)
				removeLRU();
			temp = makeNew(addr);
		}
		if (cur_count > 1)
			movetoTop(temp);
		return temp.read(addr);
	}

	public void write(long index, int data) throws IOException {
		long addr = index * RECORD_SIZE;
		BufferNode temp = checkHit(addr);
		if (temp == null) {
			if (cur_count == max_count)
				removeLRU();
			temp = makeNew(addr);
		}
		if (cur_count > 1)
			movetoTop(temp);
		temp.setDirty();
		temp.write(addr, data);
	}

	public void flush() {
		for (BufferNode cur = head; cur != null; cur = cur.getNext())
			if (cur.isDirty())
				cur.flush();
	}

	public void close() {
		try {
			file.close();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	private BufferNode makeNew(long addr) throws IOException {
		BufferNode temp = new BufferNode(addr, null, head);
		if (head != null)
			head.setPrev(temp);
		head = temp;
		if (cur_count == 0) {
			tail = temp;
		}
		cur_count++;
		return temp;
	}

	private void movetoTop(BufferNode it) {
		if (it.equals(head)) // if it is already at top, do nothing.
		{
			return;
		}
		if (it.equals(tail)) {
			// in this case, it will be the tail but the length of
			// the array will be > 1. Set prev element as the new
			// tail.
			it.getPrev().setNext(null);
			tail = it.getPrev();
		} else {
			// at this point, it cannot be the head or the tail, and therefore
			// must have valid a prev next nodes. This part essentially
			// "removes" it from the position in the array
			it.getPrev().setNext(it.getNext());
			it.getNext().setPrev(it.getPrev());
		}
		// at this point it needs to be set as the new head of the array, since
		// it was the most recently accessed node.
		it.setPrev(null);
		it.setNext(head);
		head.setPrev(it);
		head = it;
		return;
	}

	private void removeLRU() {
		if (tail.isDirty())
			tail.flush();
		if (cur_count == 1)
			head = tail = null;
		else {
			tail = tail.getPrev();
			tail.setNext(null);
		}
		cur_count--;
	}

	private BufferNode checkHit(long addr) {
		for (BufferNode cur = head; cur != null; cur = cur.getNext())
			if (cur.checkAddr(addr)) {
				hits++;
				return cur;
			}
		misses++;
		return null;
	}

	public long heapSize() throws IOException {
		return file.length() / RECORD_SIZE;
	}

	public void initCacheCount() {
		hits = misses = reads = writes = 0;
	}

	public int getHits() {
		return hits;
	}

	public int getMisses() {
		return misses;
	}

	public int getReads() {
		return reads;
	}

	public int getWrites() {
		return writes;
	}

	private class BufferNode {
		private byte[] block = new byte[BLOCK_SIZE]; // two one-dimensional
														// arrays one holding
														// key
		// values and one holding data values.
		private long addresshead;
		private boolean dirty;
		private BufferNode prev, next;

		public BufferNode(long addr, BufferNode pr, BufferNode nx)
				throws IOException {
			// block = new byte[BLOCK_SIZE];
			addresshead = addr & 0xFFFFFFFFFFFFF000l;
			file.seek(addresshead);
			file.readFully(block);
			reads++;
			prev = pr;
			next = nx;
		}

		public void flush() {
			try {
				file.seek(addresshead);
				file.write(block);
				writes++;
			} catch (IOException e) {
				e.printStackTrace();
			}
		}

		public int read(long addr) {
			int temp = 0;
			for (int i = 0; i < RECORD_SIZE; i++) {
				temp <<= 8;
				temp = temp | block[(int) addr % BLOCK_SIZE + i];
			}
			return temp;
		}

		public void write(long addr, int data) {
			for (int i = RECORD_SIZE - 1; i >= 0; i--) {
				block[(int) addr % BLOCK_SIZE + i] = (byte) (data & 0x000000FF);
				data >>>= 8;
			}
		}

		public boolean checkAddr(long addr) {
			return (addresshead >> 12) == (addr >> 12);
		}

		public boolean isDirty() {
			return dirty;
		}

		public void setDirty() {
			dirty = true;
		}

		public BufferNode getPrev() {
			return prev;
		}

		public void setPrev(BufferNode prev) {
			this.prev = prev;
		}

		public BufferNode getNext() {
			return next;
		}

		public void setNext(BufferNode next) {
			this.next = next;
		}

		public boolean equals(BufferNode it) {
			if (it == null)
				return false;
			return this.addresshead == it.addresshead;
		}
	}
}
